﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MySql.Data;
using MySql.Data.MySqlClient;
using System.Windows.Forms;
using AGPR.Locadora.Utils;

namespace AGPR.Locadora.Data
{
    /// <summary>
    /// Classe que contém os comandos comuns de um banco de dados
    /// 1º Camada - DAL (Data Access Layer)
    /// </summary>
    public sealed class ActiveRecord
    {
        /// <summary>
        /// Instância do banco de dados
        /// </summary>
        private Database db = Database.Instancia;

        #region Atributos

        /// <summary>
        /// Comando que será executado (INSERT, UPDATE...)
        /// </summary>
        private string _action = null;

        /// <summary>
        /// Tabela que a Query será executada
        /// </summary>
        private string _tabela = null;

        /// <summary>
        /// Campos da Cláusula INSERT INTO
        /// </summary>
        private string _campos = null;

        /// <summary>
        /// Valores da Cláusula INSERT INTO
        /// </summary>
        private string _valores = null;

        /// <summary>
        /// Clausula SET com campos e valores da Query
        /// </summary>
        private string _set = null;

        /// <summary>
        /// Clausula WHERE da Query
        /// </summary>
        private string _where = null;

        /// <summary>
        /// Clausula ORDER BY da Query
        /// </summary>
        private string _orderby = null;

        /// <summary>
        /// Clausula LIMIT da Query
        /// </summary>
        private string _limit = null;

        /// <summary>
        /// Query completa Parseada
        /// </summary>
        private string _query = null;

        /// <summary>
        /// Ultima Query executada
        /// </summary>
        private string _last_query = null;

        /// <summary>
        /// Tipo de Resultado a ser retornado
        /// </summary>
        private string _resultType = "command";

        #endregion

        #region Instancia > Singleton Thread-Safe
        private static volatile ActiveRecord instancia;
        private static object syncRoot = new Object();
        /// <summary>
        /// Retorna a Instancia Thread-Safe do ActiveRecord
        /// </summary>
        public static ActiveRecord Instancia
        {
            get
            {
                if (instancia == null)
                {
                    lock (syncRoot)
                    {
                        if (instancia == null)
                            instancia = new ActiveRecord();
                    }
                }

                return instancia;
            }
        }
        #endregion

        #region ActiveRecord > Construtor
        
        public ActiveRecord()
        {}

        #endregion

        #region setResultType

        /// <summary>
        /// Define o tipo de resultado a ser retorna nas consultas
        /// </summary>
        /// <param name="type">command, adapter, dataset, table</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord setResultType(string type)
        {
            this._resultType = type;

            return this;
        }

        #endregion

        #region exec > Faz o parse e inicia a execução da query

        /// <summary>
        /// Faz o parse e inicia a execução da query
        /// </summary>
        /// <returns>Resultado dinamico dependendo do tipo de consulta</returns>
        private dynamic exec()
        {
            dynamic resultado;

            this.db.conectar();
            this.parse();

            switch (this._resultType)
            {
                case "adapter":
                    resultado = this.db.queryAdapter(this._query);
                    break;

                case "dataset":
                    resultado = this.db.queryDataSet(this._query);
                    break;

                case "table":
                    resultado = this.db.queryTable(this._query);
                    break;

                case "command":
                default:
                    resultado = this.db.query(this._query);
                    break;
            }

            this.clear();

            return resultado;
        }

        #endregion

        #region Transaction > Transações

        #region startTransaction

        /// <summary>
        /// Inicia uma transação no MySQL
        /// </summary>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord startTransaction()
        {
            this._query = "START TRANSACTION;";

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        #endregion

        #region commit

        /// <summary>
        /// Faz o Commit da Transação
        /// </summary>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord commit()
        {
            this._query = "COMMIT;";

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        #endregion

        #region rollback

        /// <summary>
        /// Desfaz a transação atual
        /// </summary>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord rollback()
        {
            this._query = "ROLLBACK;";

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        #endregion

        #endregion

        #region clear > Limpa a ultima query executada

        /// <summary>
        /// Limpa a ultima query executada
        /// </summary>
        /// <returns>ActiveRecord</returns>
        private ActiveRecord clear()
        {
            this._action = null;
            this._tabela = null;
            this._campos = null;
            this._valores = null;
            this._set = null;
            this._where = null;
            this._limit = null;
            this._orderby = null;
            this._last_query = this._query;
            this._query = null;
            this._resultType = "command";

            return this;
        }

        #endregion

        #region parse > Faz o tratamento da query que será executada

        /// <summary>
        /// Faz o tratamento da query que será executada
        /// </summary>
        /// <returns>ActiveRecord</returns>
        private ActiveRecord parse()
        {
            string where = this._where != null ? " WHERE " + this._where : "";
            string limit = this._limit != null ? this._limit : "";
            string orderby = this._orderby != null ? this._orderby : "";

            switch (this._action)
            {
                case "select":
                    this._query = "SELECT " + this._campos + " FROM " + this._tabela;
                    this._query += where + orderby + limit;
                    break;

                case "count":
                    this._query = "SELECT COUNT(*) FROM " + this._tabela;
                    this._query += where + limit;
                    break;

                case "max":
                    this._query = "SELECT MAX(" + this._campos + ") FROM " + this._tabela;
                    this._query += where + limit;
                    break;

                case "insert":
                    this._query =
                        "INSERT INTO " + this._tabela + "(" + this._campos + ") VALUES (" + this._valores + ")";
                    break;

                case "update":
                    this._query = "UPDATE " + this._tabela + " SET " + this._set;
                    this._query += where;
                    break;

                case "delete":
                    this._query = "DELETE FROM " + this._tabela;
                    this._query += where;
                    break;
            }

            this._query += ";";

            return this;
        }

        #endregion

        #region where > Inclui uma clausula WHERE na ação atual

        /// <summary>Inclui uma clausula WHERE</summary>
        /// <param name="campo">Nome do Campo</param>
        /// <param name="valor">Valor do Campo</param>
        /// <param name="tipo">Tipo do Where: AND ou OR</param>
        public ActiveRecord where(string campo, dynamic valor, string tipo = "AND")
        {
            return this.where(new string[] { campo }, new string[] { Convert.ToString(valor) }, tipo);
        }

        /// <summary>Inclui várias clausulas WHERE</summary>
        /// <param name="campos">Campos</param>
        /// <param name="valores">Valores</param>
        /// <param name="tipo">Tipo do Where: AND ou OR</param>
        public ActiveRecord where(string[] campos, string[] valores, string tipo = "AND")
        {
            this._where = this._where == null ? "" : this._where + " " + tipo + " ";

            string[] where = new string[campos.Length];

            for (var i = -1; ++i < campos.Length; )
            {
                where[i] = campos[i] + " = '" + valores[i] + "'";
            }

            this._where += string.Join(" " + tipo + " ", where);

            return this;
        }

        #endregion

        #region like > Inclui uma clausula LIKE na query atual

        /// <summary>
        /// Inclui uma clausula LIKE na query atual
        /// </summary>
        /// <param name="campo">Campo</param>
        /// <param name="valor">Valor</param>
        /// <param name="tipo">Tipo</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord like(string campo, string valor, string tipo = "AND")
        {
            this._where = this._where == null ? "" : this._where + " " + tipo + " ";
            this._where += " " + campo + " LIKE '%" + valor + "%' ";

            return this;
        }

        #endregion

        #region or_like > Inclui uma clausula LIKE como OR na query atual

        /// <summary>
        /// Inclui uma clausula OR LIKE na query atual
        /// </summary>
        /// <param name="campo">Campo</param>
        /// <param name="valor">Valor</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord or_like(string campo, string valor)
        {
            return like(campo, valor, "OR");
        }

        #endregion

        #region limit > Inclui limite na query

        /// <summary>
        /// Inclui offset e limit na query atual
        /// </summary>
        /// <param name="offset">Offset da Consulta (Se o limit for igual a 0 esse entrará no lugar)</param>
        /// <param name="limit">Limite de resultados</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord limit(int offset, int limit = 0)
        {
            this._limit = " LIMIT " + offset;
            this._limit += limit != 0 ? ", " + limit : "";

            return this;
        }

        #endregion

        #region order_by > Inclui uma ordenação na query

        /// <summary>
        /// Inclui clausula ORDER BY na query atual
        /// </summary>
        /// <param name="campo">Nome do Campo que será ordenado</param>
        /// <param name="ordem">Tipo de Ordenação a ser aplicada (ASC, DESC)</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord order_by(string campo, string ordem = "ASC")
        {
            ordem = ordem == "DESC" ? ordem : "ASC";
            this._orderby = " ORDER BY " + campo + " " + ordem;

            return this;
        }

        #endregion

        #region select > Seleciona tuplas de uma Tabela

        /// <summary>Seleciona tuplas de uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="campos">Campos a serem selecionados. Por padrão todos serão.</param>
        public dynamic select(string tabela, string campos = "*")
        {
            this._action = "select";
            this._tabela = tabela;
            this._campos = campos;

            dynamic resultado;

            // Quando for do tipo comando o resultado é convertido em Lista de Dicts
            if ("command" == this._resultType)
            {
                MySqlDataReader consulta = this.exec().ExecuteReader();

                resultado = new List<dynamic>();

                // Converte o resultado do DataReader em uma Lista com os dados em Dicionário
                while (consulta.Read())
                {
                    // Dicionário de campos e valores
                    var item = new Dictionary<string, dynamic>();

                    // Popula o dicionário
                    for (int i = -1; ++i < consulta.FieldCount; )
                    {
                        var value = consulta.GetValue(i);

                        item[consulta.GetName(i)] = (value == DBNull.Value) ? null : value;
                    }

                    // Inclui a linha na lista de resultado
                    resultado.Add(item);
                }
            }
            else
            {
                resultado = this.exec();
            }
            
            this.db.desconectar();

            return resultado;
        }

        #endregion

        #region selectOne > Seleciona uma única tupla

        /// <summary>Seleciona uma única tupla</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="campos">Campos a serem selecionados. Por padrão todos serão.</param>
        public dynamic selectOne(string tabela, string campos = "*")
        {
            List<dynamic> resultado = this.limit(1).select(tabela, campos);

            return resultado.Count > 0 ? resultado[0] : null;
        }

        #endregion

        #region insert > Insere uma tupla em uma Tabela

        /// <summary>Insere uma tupla em uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="campos">Campos a serem inseridos</param>
        /// <param name="valores">Valores dos Campos</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord insert(string tabela, string[] campos, string[] valores)
        {
            this._action = "insert";
            this._tabela = tabela;
            this._campos = string.Join(", ", campos);

            // Coloco aspas simples
            for (var i = -1; ++i < valores.Length; )
            {
                valores[i] = "'" + valores[i] + "'";
            }

            this._valores = string.Join(", ", valores);

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        /// <summary>
        /// Insere uma Tupla em uma Tela no formato Dicionário
        /// </summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="dados">Dados no formato Dicionário</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord insert(string tabela, Dictionary<string, dynamic> dados)
        {
            this._action = "insert";
            this._tabela = tabela;

            List<string> campos = new List<string>();
            List<dynamic> valores = new List<dynamic>();

            foreach (KeyValuePair<string, dynamic> campo in dados)
            {
                campos.Add(campo.Key);
                valores.Add(this.DetectAndConvertType(campo.Value));
            }

            this._campos = string.Join(", ", campos);
            this._valores = string.Join(", ", valores);

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        #endregion

        #region update > Atualiza tuplas de uma Tabela

        /// <summary>Atualiza tuplas de uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="campos">Campos a serem atualizados</param>
        /// <param name="valores">Valores dos campos</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord update(string tabela, string[] campos, string[] valores)
        {
            this._action = "update";
            this._tabela = tabela;

            string[] query_set = new string[campos.Length];

            // Armazeno os valores que serão alterados na variavel temporaria query_set
            for (int i = -1; ++i < campos.Length; )
            {
                var value = this.DetectAndConvertType(valores[i]);

                query_set[i] = campos[i] + " = " + value;
            }

            this._set = string.Join(", ", query_set);

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        /// <summary>Atualiza tuplas de uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="dados">Dados a serem Atualizados</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord update(string tabela, Dictionary<string, dynamic> dados)
        {
            this._action = "update";
            this._tabela = tabela;

            List<string> query_set = new List<string>();

            foreach (KeyValuePair<string, dynamic> campo in dados)
            {
                var value = this.DetectAndConvertType(campo.Value);

                query_set.Add(campo.Key + " = " + value);
            }

            this._set = string.Join(", ", query_set);

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        /// <summary>Atualiza tuplas de uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <param name="campo">Campo a ser atualizado</param>
        /// <param name="valor">Valor do Campo</param>
        public ActiveRecord update(string tabela, dynamic campo, dynamic valor)
        {
            return this.update(tabela, 
                new string[] {Convert.ToString(campo)}, 
                new string[] {Convert.ToString(valor)});
        }

        #endregion

        #region delete > Deleta tuplas de uma tabela

        /// <summary>Deleta tuplas de uma tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <returns>ActiveRecord</returns>
        public ActiveRecord delete(string tabela)
        {
            this._action = "delete";
            this._tabela = tabela;

            this.exec().ExecuteNonQuery();
            this.db.desconectar();

            return this;
        }

        #endregion

        #region count > Conta números de tuplas numa tabela

        /// <summary>Retorna a quantidade de tuplas em uma Tabela</summary>
        /// <param name="tabela">Nome da Tabela</param>
        /// <returns>Total contado</returns>
        public int count(string tabela)
        {
            this._action = "count";
            this._tabela = tabela;
            int count = 0;

            int.TryParse(this.exec().ExecuteScalar().ToString(), out count);

            this.db.desconectar();

            return count;
        }

        #endregion

        #region max > Retorna o valor maximo

        /// <summary>Retorna o valor maximo de um campo na Tabela</summary>
        /// <param name="campo">Nome do Campo</param>
        /// <param name="tabela">Nome da Tabela</param>
        /// <returns>Valor Max</returns>
        public int max(string campo, string tabela)
        {
            this._action = "max";
            this._campos = campo;
            this._tabela = tabela;
            int max = 0;

            int.TryParse(this.exec().ExecuteScalar().ToString(), out max);

            this.db.desconectar();

            return max;
        }

        #endregion

        #region DetectAndConvertType

        /// <summary>
        /// Detecta o tipo de uma varial e a converte para um valor inserivel na base
        /// TODO: Testes
        /// </summary>
        /// <param name="variavel">Variavel a ser convertida</param>
        /// <returns></returns>
        private dynamic DetectAndConvertType(dynamic variavel)
        {
            dynamic converted = variavel;

            if (variavel != null)
            {
                string tipo = Convert.ToString(variavel.GetType());

                switch (tipo)
                {
                    case "System.Int32":
                        // Inteiro é interpretado normalmente na base
                        converted = Convert.ToInt32(variavel);
                        break;
                    case "System.Decimal":
                        // Converto separando os centavos por ponto (.), pois o padrão é por vírgula (,)
                        converted = variavel.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture);
                        break;
                    case "System.Byte":
                        converted = Convert.ToByte(variavel);
                        break;
                    case "System.Boolean":
                        converted = Convert.ToBoolean(variavel);
                        break;
                    case "System.String":
                    case "System.DateTime":
                    default:
                        converted = Convert.ToString(variavel);
                        converted = Format.limparSQL(converted);
                        converted = "'" + converted + "'";
                        break;
                }
            }
            else
            {
                converted = "NULL";
            }

            return converted;
        }

        #endregion
    }
}
